Skip to content

Map Ribo-TISH's colon-qualified ORF types on their location - #12613

Open
FelixKrueger wants to merge 4 commits into
nf-core:masterfrom
FelixKrueger:fix/orfnormalise-composite-orf-types
Open

Map Ribo-TISH's colon-qualified ORF types on their location#12613
FelixKrueger wants to merge 4 commits into
nf-core:masterfrom
FelixKrueger:fix/orfnormalise-composite-orf-types

Conversation

@FelixKrueger

Copy link
Copy Markdown
Contributor

Ribo-TISH qualifies a positional ORF-type label after a colon rather than replacing it — Novel:CDSFrameOverlap, 3'UTR:CDSFrameOverlap, Internal:CDSFrameOverlap, Novel:Known. CLASS_TOKENS holds only the bare locations and classify() matched the whole token, so all of those fell through unmatched; since #12498 made an unmatched token fatal, real Ribo-TISH output now aborts the process. This matches on the part before the first colon instead. orf_class is positional and CDSFrameOverlap describes a frame relationship rather than a position, so the qualifier does not belong in the class — and orf_type_native already carries the full token verbatim, so nothing is lost. No new vocabulary entries are needed.

Found by four nf-core/riboseq pipeline tests failing on real chr20 data, with 3–33 offending rows each.

Verification notes (AI-assisted)

Why the existing tests didn't catch it. The module fixtures use bare tokens; only genuine Ribo-TISH output produces the composite form, so #12498 was 63/63 green while this was already broken for real data. The added test uses the four tokens observed in production and fails without this change with the same message the pipeline produced:

orfnormalise: unmapped ORF type value(s) for caller 'ribotish':
  "3'UTR:CDSFrameOverlap" (1), 'Internal:CDSFrameOverlap' (1),
  'Novel:CDSFrameOverlap' (1), 'Novel:Known' (1)

Why location-only, rather than enumerating the composites. Taking the location keeps orf_class purely positional, which is the invariant #12498 established when it split length out into is_smorf. Enumerating <location>:<qualifier> pairs would need a decision per combination and would reintroduce the brittleness that exact-token matching removed. The four observed tokens resolve through the existing table with no additions:

token location orf_class
Novel:CDSFrameOverlap Novel novel_u
Novel:Known Novel novel_u
3'UTR:CDSFrameOverlap 3'UTR dORF
Internal:CDSFrameOverlap Internal intORF

Why the split is safe for every caller. Checked all 52 CLASS_TOKENS keys across the five callers: none contains a colon, so splitting cannot shorten a token that was meant to match whole. A comment on the table records that constraint for anyone adding keys later.

A location that matches nothing still fails, so the guard #12498 added keeps its value — only the qualifier is permitted to be unknown, which is what makes this robust to Ribo-TISH adding qualifiers.

nf-test test modules/nf-core/custom/orfnormalise — 11/11 pass locally (10 pre-existing plus the new one).

Ribo-TISH qualifies a positional label after a colon rather than replacing it:
Novel:CDSFrameOverlap, 3'UTR:CDSFrameOverlap, Internal:CDSFrameOverlap,
Novel:Known. CLASS_TOKENS holds only the bare locations and classify() matched
the whole token, so every one of those fell through unmatched. Since nf-core#12498 made
an unmatched token fatal, real Ribo-TISH output now aborts the process — four
nf-core/riboseq pipeline tests failed this way on chr20 data, with 3 to 33
offending rows each.

Match on the part before the first colon. orf_class is positional, and
CDSFrameOverlap describes a frame relationship rather than a position, so the
qualifier does not belong in the class; orf_type_native already carries the full
token verbatim, so nothing is lost and the harmonisation stays auditable. No
CLASS_TOKENS key contains a colon, so this cannot shorten a token meant to match
whole, and no new vocabulary entries are needed: the four tokens resolve to
novel_u, dORF and intORF through the existing table.

The module tests missed this because their fixtures use bare tokens; only real
Ribo-TISH output produces the composite form. The added test uses the four
observed tokens and fails without this change with the same message the pipeline
produced.
FelixKrueger added a commit to nf-core/riboseq that referenced this pull request Aug 6, 2026
The workflow is registered (id 328515623) from its 08-06 run, so the dispatch API
resolves it by numeric ID and workflow_dispatch is enough to start it on demand.
Dispatch by filename would not work: that lookup requires the file to be on the
default branch, and this one deliberately never reaches master.

The push trigger is dropped rather than kept alongside. Pushing this commit would
otherwise have had a chance of starting the four 45-minute jobs, and they cannot
pass until nf-core/modules#12613 has merged and the SHAs are re-bumped.
The original checks for this head wedged during the 2026-08-06 Actions incident:
queued with zero jobs created, and refusing both cancel ("already completed")
and rerun ("already running"). No API path revives them, so a new head is the
only way to get CI. The tree is unchanged, so the local 11/11 verification still
describes exactly this content.
@FelixKrueger

Copy link
Copy Markdown
Contributor Author

CI is red here but none of it belongs to this PR: all four failures are PIRATE, and they split by container engine — PIRATE passes on docker in this same run and fails only on conda and singularity. This PR changes Python inside a module template, which is engine-agnostic, so a failure that appears on two engines and not the third cannot be coming from it. On singularity the log says Failed to pull singularity image; on conda PIRATE runs but writes its own fail_test.txt instead of its expected outputs. Meanwhile CUSTOM_ORFNORMALISE ran in 33 shards and passed in every one, including the new test ribotish composite ORF types map on the location, keeping the qualifier native, and BWAMEM3_MEM and STRINGTIE_STRINGTIE passed alongside PIRATE in the same failing shards, so the runners are healthy. One other thing worth flagging: the head is 4a6b135f rather than d4d18fce because the original checks wedged during the 2026-08-06 Actions incident — that commit is empty and the tree is byte-identical. Happy to wait if you'd rather not merge past an unrelated red; nothing here needs a change on my side.

Evidence (AI-assisted)

The four failing checks, out of 72 check runs across 7 suites:

Check Cause
x64 | conda | 5 PIRATE snapshot mismatch
x64 | singularity | 5 PIRATE snapshot mismatch + Failed to pull singularity image
x64 | singularity | 6 same pair
confirm-pass-nf-test downstream aggregate, fails because the above did

x64 | docker | 5 and x64 | docker | 6 both passed — same shard numbers, so the same tests, on the third engine. That is the load-bearing observation.

PIRATE's diff shows the tool deciding it failed, not snapshot drift — expected outputs absent, failure marker present:

-    "PIRATE.gene_families.ordered.tsv",
-    "PIRATE.gene_families.tsv",
-    "PIRATE.pangenome_summary.txt",
-    "cluster_alleles.tab",
     "PIRATE.log",
+    "fail_test.txt",
     "genome2loci.tab",

This module's tests. Test Process CUSTOM_ORFNORMALISE appears in 33 shard jobs, every one with conclusion success and no FAILED lines. Confirmed by test name rather than by aggregate, since this repo shards at roughly one test per shard:

'ribotish composite ORF types map on the location, keeping the qualifier native'
'homo_sapiens [chr20] - ribotish'
'homo_sapiens [chr20] - ribotish + ribocode'
'homo_sapiens [chr20] - ribotish + ribocode - collapse disabled'
'homo_sapiens [chr20] - ribotish - score-field override'

Why the head moved. The five original checks on d4d18fce wedged during the Actions incident: queued with zero jobs created, refusing both recovery verbs with contradictory errors — cancel reported "already completed", rerun reported "already running" (403). With no API route to revive them, a new head was the only way to get CI. 4a6b135f re-parents the same tree (276fd8f25), so the content under test is unchanged and the local verification still describes it. Those five will read queued on d4d18fce indefinitely and gate nothing.

Local verification, unchanged from the PR description: 11/11 including a negative control — reverting only classify() makes the new test fail with the exact production error message.

@FelixKrueger

Copy link
Copy Markdown
Contributor Author

Merging master in a563217c cleared the PIRATE failures without touching orfnormalise — now green over 56 checks (50 passed, 6 skipped, none failed). So the attribution above is settled empirically too, and this is ready for review.

@FelixKrueger
FelixKrueger enabled auto-merge August 9, 2026 10:58
"""
input[0] = channel
.of(
'Tid\tGid\tGenomePos\tTisType\tAALen\tFisherPvalue',

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you have a real input we could use as a proper fixture in test-datasets? The existing one (genomics/homo_sapiens/riboseq_expression/orf_predictions/sample1.ribotish.pred.txt) obviously isn't representative enough, so we should consider adding another one rather than embedding dummy data here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes — real output with these tokens exists, so this needn't stay synthetic. The composite labels are what made a pipeline-level snapshot-recording run fail on genuine chr20 Ribo-TISH output, with this distribution:

Novel:CDSFrameOverlap     29
Novel:Known                4
3'UTR:CDSFrameOverlap      1
Internal:CDSFrameOverlap   1

That is also why the module tests missed the gap — the existing fixture only contains bare tokens, exactly as you say.

Two choices I would rather you made before I open the test-datasets PR, since both affect other modules:

  1. A second file, or make the existing one representative? My instinct is to add a new file (sample1.ribotish.composite.pred.txt or similar) rather than modify sample1.ribotish.pred.txt in place. Editing a shared fixture is what briefly broke custom/orfcollapse when Refresh the custom/orfcollapse fixture for the positional orf_class vocabulary test-datasets#2193 merged — there is no version gate, so every consumer picks the change up immediately and only notices when something next touches it. But if you would rather the existing fixture simply became representative, that is fine too and I will sequence the module PRs behind it.
  2. Region. The existing sample1.* family is chr20, and real composite tokens are plentiful there, so the family can stay on one region unless you want otherwise.

Once you have picked, the order is test-datasets first, then this PR swaps the inline rows for the fixture. Happy to do both.

@pinin4fjords pinin4fjords Aug 9, 2026

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We try not to overwrite files in test-datasets, they should really be write-once (though it's patchily followed). Add a new one.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added as a new file in nf-core/test-datasets#2208, generated by running the pipeline on its own chr20 data in extended-ORF mode. Worth flagging: real output has seven colon-qualified forms, not the four we knew about -- 5'UTR:Known, 5'UTR:CDSFrameOverlap and Truncated:Known also occur, and all seven already resolve through the existing table. I will point the test at the fixture and drop the inline rows once that merges.

part before the first colon describes where the ORF sits, and `orf_class`
is positional, so the qualifier is dropped here and preserved verbatim in
`orf_type_native`. No CLASS_TOKENS key contains a colon, so this cannot
shorten a token that was meant to match whole.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we reduce the AI historical narrative in the comments? I get this problem a lot, but comments should be stand-alone and represent the current state, not reference a previous state you're fixing.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Trimmed in 45d5122 — comment-only across the three files, and no executable change: orfnormalise.py's token stream is identical apart from the docstring literal itself.

  • classify() docstring: the paragraph describing what the previous code did is gone. It now states only that the part before the first colon is matched and that the full token survives in orf_type_native.
  • CLASS_TOKENS header: the Ribo-TISH narrative is replaced by the single constraint a future editor of that table actually needs — No key may contain a colon: classify() matches only the part before one. That invariant is load-bearing (a colon-bearing key would be silently truncated by the split), so I kept it, but moved it onto the table it constrains rather than leaving it as a justification in the docstring.
  • meta.yml and the new test assertion: same treatment.

One thing I left alone: there is a pre-existing instance of exactly this pattern a few lines above, from #12498Substring matching mis-fired on the overlap forms, because "uorf" is a substring of "overlap_uorf". Happy to fold that in here if you'd like it gone now; I left it out so this PR stays scoped to comments it introduced itself.

Review asked for comments that stand alone rather than describe what the
previous code did. The classify() docstring, the CLASS_TOKENS header,
meta.yml and the new test now state current behaviour only.

The colon-free-key invariant is kept, moved onto the table it constrains
so it is read when that table is edited.

Comment-only: orfnormalise.py's token stream is unchanged apart from the
docstring literal (5877 tokens before and after, one differing).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants